use v6;
unit module SSH;

my $mkpts;

INIT {
	if shell('which mkpts >/dev/null 2>&1').exitcode == 0 {
		$mkpts = 'mkpts';
	} else {
		if shell('which gcc >/dev/null 2>&1').exitcode == 0 {
			my $src = "/tmp/mkpts-{$*PID}.c";
			spurt $src, q:to/EOF/;
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <string.h>
extern char const *ptsname(int);
extern int posix_openpt(int);
extern int grantpt(int);
extern int unlockpt(int);

static
int event;

struct
{
  int help;
  int f_err;
  int verbose;
  int no_echo;
  int row, col;
} static options;

static
void
on_hup(int __)
{
  int a = write(event, "a", 1);
  (void)a;
}

static
int
transfer(int from, int to)
{
  char buf[2048];
  ssize_t r = read(from, buf, sizeof(buf));
  if (r < 0)
    return 1;

  if (options.verbose)
    {
      if (from == 0)
        fprintf(stderr, ">>>> %zd\n", r);
      else
        fprintf(stderr, "<<<< %zd\n", r);
    }
  
  ssize_t w = 0;
  while (w < r)
    {
      ssize_t n = write(to, buf + w, r - w);
      if (n <= 0)
        return 1;

      w += n;
    }
  return 0;
}

static
void
giveup_tty()
{
  int tty = open("/dev/tty", O_RDWR);
  if (tty >= 0)
    {
      ioctl(tty, TIOCNOTTY);
      close(tty);
    }
}

static
void
set_tty(int fd)
{
  setsid();
  ioctl(fd, TIOCSCTTY, 0);
}

static
void
set_noecho(int fd)
{
  struct termios ts, ts_old;
  tcgetattr(fd, &ts_old);
  ts = ts_old;
  ts.c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL);
  tcsetattr(fd, TCSAFLUSH, &ts);
}

static
void
set_winsize(int fd, int row, int col)
{
  struct winsize size;
  size.ws_row = row;
  size.ws_col = col;
  size.ws_xpixel = size.ws_ypixel = 0;
  ioctl(fd, TIOCSWINSZ, &size);
}

static
void
cp_winsize(int src, int dst)
{
  struct winsize size;
  ioctl(src, TIOCGWINSZ, &size);
  ioctl(dst, TIOCSWINSZ, &size);
}

static
void
usage()
{
}

static
char **
parse_args(char **argv)
{
  char *a;
  for (; (a = *argv) && *a++ == '-'; ++argv)
    {
      if (!*a)
        {
          ++ argv;
          break;
        }

      char b;
      int loop = 1;
      while (loop && (b = *a++))
        {
          switch (b)
            {
            case 'h':
              options.help = 1;
              goto end;

            case 'v':
              options.verbose = 1;
              break;

            case '2':
              options.f_err = 1;
              break;

            case 'n':
              options.no_echo = 1;
              break;

            case 's':
              sscanf(a, "%dx%d", &options.row, &options.col);
              loop = 0;
              break;

            default:
              fprintf(stderr, "option error: %c\n", b);
              break;
            }
        }
    }
 end:
  return argv;
}

int
main(int argc, char **argv)
{
  if (argc < 2)
    {
      usage();
      return 1;
    }
  memset(&options, 0, sizeof(options));
  argv = parse_args(argv + 1);
  if (options.help)
    {
      usage();
      return 0;
    }
  if (!argv[0])
    {
      usage();
      return 1;
    }
  
  int mts = posix_openpt(O_RDWR);
  if (mts < 0)
    {
      fprintf(stderr, "posix_openpt error\n");
      return 1;
    }
  grantpt(mts);
  unlockpt(mts);
  int pts = open(ptsname(mts), O_RDWR);
  if (pts < 0)
    {
      fprintf(stderr, "open pts error\n");
      return 1;
    }
  if (options.no_echo)
    set_noecho(pts);

  if (options.row > 0 && options.col)
    set_winsize(pts, options.row, options.col);
  else
    {
      int cty = open("/dev/tty", O_RDWR);
      if (cty >= 0)
        {
          cp_winsize(cty, pts);
          close(cty);
        }
    }
  
  int pid = fork();
  if (pid == 0)
    {
      dup2(pts, 0);
      dup2(pts, 1);

      if (options.f_err == 0)
        dup2(pts, 2);
      
      giveup_tty();
      set_tty(pts);
      close(pts);
	  close(mts);
      execvp(argv[0], argv);
      return 127;
    }
  else
    {
      close(pts);
      event = eventfd(0, 0);
      signal(SIGCHLD, on_hup);
      while (1)
        {
          struct pollfd f[3];
          f[0].fd = event;
          f[0].events = POLLIN;
          f[0].revents = 0;
          f[1].fd = mts;
          f[1].events = POLLIN;
          f[1].revents = 0;
          f[2].fd = 0;
          f[2].events = POLLIN;
          f[2].revents = 0;
          
          int c = poll(f, sizeof(f)/sizeof(f[0]), 1000*60);
          if (c > 0)
            {
              if (f[0].revents & POLLIN)
                break;
              
              if ((f[1].revents & (POLLERR|POLLHUP)) ||
                  (f[2].revents & (POLLERR|POLLHUP)))
                break;

              if ((f[1].revents & POLLIN) && transfer(mts, 1))
                break;
              if ((f[2].revents & POLLIN) && transfer(0, mts))
                break;
            }
          else if (c < 0)
            break;
        }
      return 0;
    }
}
EOF
            my $out = "/tmp/mkpts-{$*PID}";
            shell "gcc -O2 -o $out $src >/dev/null 2>&1";
            $src.IO.unlink;
			$mkpts = $out;
		} else {
			$*ERR.put: "no mkpts"
		}
	}
}

END {
	$mkpts && $mkpts.IO.unlink
}


class SSHConnetion is export {
	has $.proc;
	has $.prom;
	has $.pout;

	method close() {
		$.proc.kill: SIGTERM;
		await($.prom).exitcode
	}

	method exec(SSHConnetion:D: Str $cmd) {
		my $buff = '';
		$.proc.put: $cmd;
		my $result;
		react {
			whenever $.pout.Supply -> $n {
				if $n ~~ / (.*) '$>' / {
					$result = $buff ~ $0;
					if $result ~~ /^ \s (.*) $/ {
						$result = $0
					}
					done
				} else {
					$buff ~= $n;
				}
			}
			whenever $.prom.Supply {
				done
			}
		}
		$result
	}
}

sub ssh_connect(Str $host, Str $pass) is export {
	$mkpts || return Nil;
	my $proc = Proc::Async.new(:w, $mkpts, '-n', 'ssh',
							   $host);
	my $pout = Supplier.new;
	$proc.stdout.tap: { $pout.emit: $_; }
	my $out_buff = '';
	my $run = $proc.start;
	my $result;
	react {
		whenever $pout.Supply {
			$out_buff ~= $_;
			given $out_buff {
				when /:i yes \W+ no/ {
					$proc.put: 'yes';
					$out_buff = '';
				}
				when /:i password ':' / {
					$proc.put: $pass;
					$out_buff = '';
				}
				when /:i denied .* publickey / {
					done
				}
				when / 'Last login:' / {
					$out_buff = '';
					$proc.put: q/PS1='$> '/;
				}
				when / '$>' / {
					$result = SSHConnetion.new(proc => $proc,
											   prom => $run,
											   pout => $pout);
					done
				}
			}
		}
		whenever $run.Supply {
			done
		}
	}
	$result
}


sub scp(Str $pass, *@args) is export {
	$mkpts || return Nil;
	my $proc = Proc::Async.new(:w, $mkpts, '-n', 'scp', |@args);
	my $pout = Supplier.new;
	$proc.stdout.tap: { $pout.emit: $_; }
	my $out_buff = '';
	my $run = $proc.start;
	react {
		whenever $pout.Supply {
			$out_buff ~= $_;
			given $out_buff {
				when /:i yes \W+ no/ {
					$proc.put: 'yes';
					$out_buff = '';
				}
				when /:i password / {
					$proc.put: $pass;
					$out_buff = '';
					done
				}
			}
		}
		whenever $run.Supply {
			done
		}
	}
	await($run).exitcode
}
